package org.apache.solr.ocean.core;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.StaleReaderException;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.search.ExtendedFieldCache;
import org.apache.lucene.search.HitCollector;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.FieldCache.StringIndex;
import org.apache.solr.ocean.cache.Cache;
import org.apache.solr.ocean.cache.CacheKey;
import org.apache.solr.ocean.cache.DocSetCacheKey;
import org.apache.solr.ocean.core.Deletes.DeleteByQuery;
import org.apache.solr.ocean.core.TransactionSystem.Category;
import org.apache.solr.ocean.util.Constants;
import org.apache.solr.ocean.util.IndexUtil;
import org.apache.solr.ocean.util.Timeout;
import org.apache.solr.ocean.util.Util;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.search.DocSet;

public abstract class Index {
  private final Long id;
  protected final IndexSchema indexSchema;
  protected boolean isClosed = false;
  private final TransactionSystem.Category category;
  private boolean isReadOnly = false;
  private boolean isDeleteOnly = false;

  public Index(Long id, IndexSchema indexSchema, TransactionSystem.Category category) {
    this.id = id;
    this.indexSchema = indexSchema;
    this.category = category;
  }

  protected Cache initCache() {
    OceanCategoryConfig config = category.getConfig();
    int docSetMax = config.getDocSetCacheMax(100);
    return new Cache(docSetMax);
  }

  public static IndexReader[] getIndexReaders(List<? extends IndexSnapshot> indexSnapshots) {
    IndexReader[] indexReaders = new IndexReader[indexSnapshots.size()];
    for (int x = 0; x < indexSnapshots.size(); x++) {
      indexReaders[x] = indexSnapshots.get(x).getIndexReader();
    }
    return indexReaders;
  }

  public static Long getHighestSnapshotId(List<? extends IndexSnapshot> indexSnapshots) throws Exception {
    List<Long> snapshotIdList = new ArrayList<Long>(indexSnapshots.size());
    for (IndexSnapshot indexSnapshot : indexSnapshots) {
      snapshotIdList.add(indexSnapshot.getHighestSnapshotId());
    }
    return Collections.max(snapshotIdList);
  }

  public static Long getHighestDocumentId(List<? extends IndexSnapshot> indexSnapshots) throws Exception {
    List<Long> documentIdList = new ArrayList<Long>(indexSnapshots.size());
    for (IndexSnapshot indexSnapshot : indexSnapshots) {
      documentIdList.add(indexSnapshot.getHighestDocumentId());
    }
    return Collections.max(documentIdList);
  }

  public TransactionSystem.Category getCategory() {
    return category;
  }

  public void setDeleteOnly(boolean isDeleteOnly) {
    this.isDeleteOnly = isDeleteOnly;
  }

  public boolean isDeleteOnly() {
    return isDeleteOnly;
  }

  public boolean isReadOnly() {
    return isReadOnly;
  }

  public void setReadOnly(boolean isReadOnly) {
    this.isReadOnly = isReadOnly;
  }

  protected DeletesResult applyDeletes(boolean deleteFromReader, Deletes deletes, Collection<Integer> deletedDocs, IndexReader indexReader,
      IndexSchema indexSchema) throws CorruptIndexException, IOException, Exception {
    long[] ids = ExtendedFieldCache.EXT_DEFAULT.getLongs(indexReader, Constants.ID);
    DeletesResult deletesResult = new DeletesResult(getId());
    if (deletes.hasDeletes()) {
      if (deletes.hasDocIds()) {
        int docsDeleted = 0;
        long[] docIdsArray = deletes.getDocIds();
        for (long id : docIdsArray) {
          int doc = IndexUtil.getDoc(Constants.ID, id, indexReader, indexSchema);
          indexReader.deleteDocument(doc);
          docsDeleted++;
        }
        deletesResult.add(new DeletesResult.Result(docIdsArray, docsDeleted));
      } else {
        List<Long> docIds = deletesResult.getDocIds();
        if (deletes.hasTerms()) {
          List<Term> terms = deletes.getTerms();
          for (Term term : terms) {
            int docsDeleted = deleteByTerm(deleteFromReader, term, deletedDocs, docIds, ids, indexReader);
            deletesResult.add(new DeletesResult.Result(term, docsDeleted));
          }
        }
        if (deletes.hasDeleteByQueries()) {
          List<DeleteByQuery> deleteByQueries = deletes.getDeleteByQueries();
          for (DeleteByQuery deleteByQuery : deleteByQueries) {
            int docsDeleted = deleteByQuery(deleteFromReader, deleteByQuery, deletedDocs, ids, docIds, indexReader);
            deletesResult.add(new DeletesResult.Result(deleteByQuery, docsDeleted));
          }
        }
      }
    }
    return deletesResult;
  }

  protected int deleteByTerm(boolean deleteFromReader, Term term, Collection<Integer> deletedDocs, List<Long> docIds, long[] ids,
      IndexReader indexReader) throws IOException {
    TermDocs docs = indexReader.termDocs(term);
    if (docs == null)
      return 0;
    int n = 0;
    try {
      while (docs.next()) {
        int doc = docs.doc();
        if (deletedDocs != null)
          deletedDocs.add(doc);
        Long docId = ids[doc];
        docIds.add(docId);
        if (deleteFromReader)
          indexReader.deleteDocument(doc);
        n++;
      }
    } finally {
      docs.close();
    }
    return n;
  }

  protected int deleteByQuery(final boolean deleteFromReader, DeleteByQuery deleteByQuery, final Collection<Integer> deletedDocs,
      final long[] ids, final List<Long> deletedIds, final IndexReader indexReader) throws IOException {
    Query query = deleteByQuery.getQuery();
    IndexSearcher indexSearcher = new IndexSearcher(indexReader);
    final int[] numDeleted = new int[1];
    indexSearcher.search(query, new HitCollector() {
      public void collect(int doc, float score) {
        try {
          if (deleteFromReader)
            indexReader.deleteDocument(doc);
          if (deletedDocs != null)
            deletedDocs.add(doc);
          Long docId = ids[doc];
          deletedIds.add(docId);
          numDeleted[0]++;
        } catch (StaleReaderException staleReaderException) {
          throw new RuntimeException(staleReaderException);
        } catch (IOException ioException) {
          throw new RuntimeException(ioException);
        }
      }
    });
    return numDeleted[0];
  }

  public static class DeletesResult {
    private long indexId;
    private List<Result> results = new ArrayList<Result>();
    private int numDeleted = 0;
    private List<Long> docIds = new LinkedList<Long>();

    public DeletesResult(long indexId) {
      this.indexId = indexId;
    }

    public List<Long> getDocIds() {
      return docIds;
    }

    public Long getIndexId() {
      return indexId;
    }

    public void add(Result result) {
      numDeleted += result.getNumDeleted();
      results.add(result);
    }

    public int getNumDeleted() {
      return numDeleted;
    }

    public static class Result {
      private Object delete;
      private int numDeleted;

      public Result(Object delete, int numDeleted) {
        this.delete = delete;
        this.numDeleted = numDeleted;
      }

      public Object getDelete() {
        return delete;
      }

      public int getNumDeleted() {
        return numDeleted;
      }
    }
  }

  public static class IndexException extends Exception {
    public IndexException(String message) {
      super(message);
    }

    public IndexException(String message, Exception exception) {
      super(message, exception);
    }
  }

  public static class IndexNeverCompletedCopyException extends IndexException {
    public IndexNeverCompletedCopyException(String message) {
      super(message);
    }
  }

  public IndexSchema getIndexSchema() throws Exception {
    return indexSchema;
  }

  public abstract IndexSnapshot getIndexSnapshot(Long snapshotID);

  public abstract IndexSnapshot getLatestIndexSnapshot();

  public abstract class IndexSnapshot {
    protected final Long snapshotId;
    protected final IndexSchema indexSchema;

    public IndexSnapshot(Long snapshotId, IndexSchema indexSchema) {
      this.snapshotId = snapshotId;
      this.indexSchema = indexSchema;
    }

    public abstract int maxDoc();

    public Long getSnapshotId() {
      return snapshotId;
    }

    public Index getIndex() {
      return Index.this;
    }

    public IndexSchema getIndexSchema() {
      return indexSchema;
    }

    public String getHighest(String field) throws Exception {
      StringIndex stringIndex = ExtendedFieldCache.EXT_DEFAULT.getStringIndex(getIndexReader(), field);
      return stringIndex.lookup[stringIndex.lookup.length-1];
    }

    public Long getHighestSnapshotId() throws Exception {
      String string = getHighest(Constants.SNAPSHOTID);
      return Util.getLongFromIndexed(Constants.SNAPSHOTID, string, indexSchema);
    }

    public Long getHighestDocumentId() throws Exception {
      String string = getHighest(Constants.DOCUMENTID);
      return Util.getLongFromIndexed(Constants.DOCUMENTID, string, indexSchema);
    }

    public abstract Set<CacheKey> getCacheKeys();

    public abstract IndexReader getIndexReader();

    public abstract DocSet getDocSet(DocSetCacheKey cacheKey, Timeout timeout) throws Exception;

    public abstract boolean containsKey(CacheKey cacheKey);
  }

  public static class MergedDocMap {
    private Map<IndexSnapshot,int[]> oldMap; // maps old doc to new doc
    private RI[] merged; // maps new doc to old doc and reader

    public MergedDocMap(List<? extends IndexSnapshot> indexSnapshots) {
      int newMaxDoc = 0;
      for (IndexSnapshot indexSnapshot : indexSnapshots) {
        newMaxDoc += indexSnapshot.getIndexReader().numDocs();
      }
      oldMap = new HashMap<IndexSnapshot,int[]>(indexSnapshots.size());
      RI[] merged = new RI[newMaxDoc];
      int pos = 0;
      for (IndexSnapshot indexSnapshot : indexSnapshots) {
        IndexReader indexReader = indexSnapshot.getIndexReader();
        int maxDoc = indexReader.maxDoc();
        int[] old = new int[maxDoc];
        for (int x = 0; x < maxDoc; x++) {
          if (indexReader.hasDeletions() && indexReader.isDeleted(x)) {
            merged[pos] = null;
            old[x] = -1;
          } else {
            merged[pos] = new RI(x, indexSnapshot);
            old[x] = pos;
            pos++;
          }
        }
        oldMap.put(indexSnapshot, old);
      }
    }

    public static class RI {
      public int doc;
      public IndexSnapshot oldIndexSnapshot;

      public RI(int doc, IndexSnapshot oldIndexSnapshot) {
        this.doc = doc;
        this.oldIndexSnapshot = oldIndexSnapshot;
      }
    }

    public Map<IndexSnapshot,int[]> getOldMap() {
      return oldMap;
    }

    public RI[] getMerged() {
      return merged;
    }
  }
  /**
  public abstract static class FieldCursor {
    public abstract boolean next() throws Exception;

    public abstract DocIdSetIterator docs() throws Exception;

    public abstract Object value() throws Exception;

    public abstract void close() throws Exception;
  }
  **/
  /**
   * for locking a disk index snapshot for index file replication
   */
  public static class NodeRef {
    private String id;
    private URL url;

    public NodeRef(String id, URL url) {
      this.id = id;
      this.url = url;
    }

    public String getId() {
      return id;
    }

    public URL getUrl() {
      return url;
    }
  }

  public boolean isClosed() {
    return isClosed;
  }

  public Long getId() {
    return id;
  }

  public abstract DeletesResult commitDeletes(Deletes deletes, Transaction transaction) throws IndexException, InterruptedException,
      IOException;
}
